package com.eugenes.concurrency.util;

import com.eugenes.jdbc.IPseudoConnection;
import com.eugenes.jdbc.PseudoConnection;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class ConnectionPool {

    private final static int WAITING_TIMEOUT = 5 * 1000;
    private final static TimeUnit WAITING_TIMEOUT_UNIT = TimeUnit.MILLISECONDS;

    private final String url;
    private final String username;
    private final String password;
    private final int size;

    private final Semaphore semaphore;
    private final Set<IPseudoConnection> connectionsInUse;
    private final List<IPseudoConnection> connectionsInIdle;

    private ConnectionsDaemon daemon;

    private ConnectionPool(String url, String username, String password, int n) {
        this.connectionsInUse = new HashSet<>();
        this.connectionsInIdle = new ArrayList<>();
        this.url = url;
        this.username = username;
        this.password = password;
        size = n;
        semaphore = new Semaphore(n);
    }

    public String getUrl() {
        return url;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

    public int getSize() {
        return size;
    }

    private IPseudoConnection getFreeConnection() {
        IPseudoConnection connection;
        synchronized (connectionsInIdle) {
            if (connectionsInIdle.isEmpty()) {
                connectionsInIdle.add(new PseudoConnection());
            }
            connection = connectionsInIdle.remove(connectionsInIdle.size() - 1);
        }
        return connection;
    }

    private IPseudoConnection createNewWrappedConnection(IPseudoConnection connection) {
        return new ConnectionWrapper(connection);
    }

    private void putConnectionInUse(IPseudoConnection connection) {
        assert (connection instanceof ConnectionWrapper);
        synchronized (connectionsInUse) {
            connectionsInUse.add(connection);
        }
    }

    public IPseudoConnection checkout() throws InterruptedException,
            TimeoutException {
        boolean acquired;
        acquired = semaphore.tryAcquire(WAITING_TIMEOUT, WAITING_TIMEOUT_UNIT);

        // In case of InterruptedException this condition won't be checked
        // because interrupt is not handled by this method. So throw
        // TimeoutException.
        if (!acquired) {
            throw new TimeoutException("Timeout of checkout method has run out");
        }

        IPseudoConnection connection = getFreeConnection();
        IPseudoConnection wc = createNewWrappedConnection(connection);
        wc.checkout();
        putConnectionInUse(wc);
        return wc;
    }

    private void removeConnectionFromUse(IPseudoConnection connection) {
        synchronized (connectionsInUse) {
            if (!connectionsInUse.contains(connection)) {
                throw new IllegalArgumentException("Connection checkin with wrong connection.");
            }

            connectionsInUse.remove(connection);
        }
    }

    private void putConnectionInIdle(IPseudoConnection connection) {
        synchronized (connectionsInIdle) {
            connectionsInIdle.add(connection);
        }
    }

    // May be some problems when connection returned twice (IllegalStateConnection).
    // We can provoke this problem with daemon thread which can return connection
    // twice (in case it was not removed from connectionsInUse in 30 seconds, very
    // slow performance)
    public void checkin(IPseudoConnection connection) {
        removeConnectionFromUse(connection);
        IPseudoConnection realConnection;
        realConnection = ((ConnectionWrapper)connection).returnConnection();
        putConnectionInIdle(realConnection);
        semaphore.release();
    }

    public void close() {
        daemon.finish = true;
    }

    public synchronized static ConnectionPool getConnectionPool(String url, String userName, String password, int n) {
        ConnectionPool pool = new ConnectionPool(url, userName, password, n);
        ConnectionsDaemon cd = pool.new ConnectionsDaemon();
        pool.daemon = cd;
        new Thread(cd).start();
        return pool;
    }

    private class ConnectionWrapper implements IPseudoConnection {
        private IPseudoConnection connection;
        private volatile long lastAccessTime;
        private volatile int inMethodCounter;

        public ConnectionWrapper(IPseudoConnection connection) {
            this.connection = connection;
        }

        void checkAccess() {
            if (connection == null) {
                throw new IllegalStateException("Connection is not owned by thread.");
            }
        }

        long getLastAccessTime() {
            return lastAccessTime;
        }

        boolean isInUse() {
            return inMethodCounter > 0;
        }

        IPseudoConnection returnConnection() {
            IPseudoConnection result = connection;
            connection = null;
            return result;
        }

        @Override
        public void checkout() {
            checkAccess();
            lastAccessTime = System.currentTimeMillis();
            connection.checkout();
        }

        @Override
        public void use() {
            checkAccess();
            ++inMethodCounter;
            connection.use();
            --inMethodCounter;
        }
    }

    private class ConnectionsDaemon implements Runnable {

        private final static long SLEEPTIME = 30 * 1000 /*ms*/;
        private final static long IDLETIME = 10 * 1000 /*ms*/;

        private volatile boolean finish;

        @Override
        public void run() {
            while (!finish) {
                try {
                    Thread.sleep(SLEEPTIME);
                    for (IPseudoConnection c : connectionsInUse) {
                        ConnectionWrapper cw = (ConnectionWrapper)c;
                        if (!cw.isInUse() &&
                                cw.getLastAccessTime() < System.currentTimeMillis() - IDLETIME) {

                            checkin(c);
                        }
                    }
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }
    }
}
